物件導向是現代程式語言很被重視的能力,Javascript可以怎樣支援物件導向呢?
封裝(encapsulation)
封裝的目的是要隱藏實作的細節,只讓抽象的介面暴露出來。對一般使用者來說,只要知道如何操作這些介面就可以了。
但是在Javascript中,物件的property與method都是...公開的,而且可以輕易被覆寫。像下面的例子:
if (!oldOpen) {
oldOpen = window.open;
window.open = function(u,t,o) {
return null;
};
}
這樣就會覆寫過window物件的open()方法,接下來網頁中的javascript就無法另外開啟視窗了。這是過去防毒軟體跟瀏覽器常用的擋廣告popup視窗的方法。(現在應該不是這樣搞的)
有一個簡單的方法可以達到封裝的目的,就是透過變數及函數。
像這個簡單的例子:(iron009.html)
var F = function() {
var name;
this.setName = function(v) {name=v;};
this.getName = function() {return name;};
};
var a = new F();
a.setName('test');
alert(a.getName());
因為變數範圍的關係,在F函數之外沒有辦法直接存取name這個變數,只能透過getName()跟setName()方法。
不過這樣有一個壞處,就是基於Javascript的Lexical Scope特性,如果setName()與getName()是在F之外用F.prototype.setName=function(){...}以及F.prototype.getName = function(){...}這樣的方式來定義時,他們就無法存取name變數。如果要可以做到,那得多花一些功夫用Function物件的call或apply函數來實現:
var F = function() {
var name;
this.fn = {};
function iter2a(it) {
var i=0;
str = '';
for (; i<it.length; i++) {
if (!i == (it.length-1)) {
str += it[i] + ",";
} else {
str += it[i];
}
}
return str.split(',');
}
function enum(em) {
var str = '';
for (var i in em) {
str += '[' + i + "]\n";
}
return str;
}
this.extend = function(n,f) {
if (typeof this[n] == 'undefined') {
this[n] = function() {
return f.apply(this, iter2a(arguments));
}
}
}
};
var a = new F();
a.extend('getName',function(){return name;});
a.extend('setName',function(v){name=v;});
a.setName('test');
alert(a.getName());
(iter2a這個函數是要把一些list轉成真正的陣列,enum則是列舉物件的property,作為內部測試使用)
除了使用變數,其實直接用function而不是用this.functionname = function來定義的函數,也只有在F之中才能存取。但是這些在使用透過extend方法加入的方法中,因為使用了apply改變了他運作的變數範圍,所以就可以直接存取。
簡單地說,用上述的方法,就可以把本地變數跟函數,當作private property及private method來用,達到封裝的目標。而透過使用extend方法加入的操作,仍然可以使用到這些private的資源。
繼承(inheritance)
Javascript支援prototype base的繼承方式,這些都是透過函數物件來做,所以Javascript的函數物件也可以把當看作類似class的角色。
最基本的繼承語法像這樣:
function Parent() {
this.foo = function(){
alert('Parent.foo');
};
}
Parent.prototype.bar = function() {
alert('Parent.bar');
}
function Child() {
this.myfoo = function() {
alert('Child.myfoo');
};
}
Child.prototype = new Parent();
var a = new Child();
a.foo();
a.bar();
a.myfoo();
(從Netscape的Core Javascript Guide開始就這樣建議這樣達到繼承的目的)
但是其實可以用更「手工」的方法:
function Parent() {
this.foo = function(){
alert('Parent.foo');
};
}
Parent.prototype.bar = function() {
alert('Parent.bar');
}
function Child() {
Parent.call(this)
this.myfoo = function() {
alert('Child.myfoo');
};
}
Child.prototype = Parent.prototype;
var a = new Child();
a.foo();
a.bar();
a.myfoo();
兩個方法比較一下,更能了解Javascript的prototype繼承機制。簡單地說new Parent()這個動作,會執行Parent這個函數的動作產生一個物件,然後把Parent的prototype屬性指派給它後回傳。所以Child.prototype裡面,就是這個新產生的物件,以及這個物件的prototype屬性。之後在執行new Child()時也是一樣的。
多型(polymorphism)
javascript並不支援函數/方法參數的多型,只能支援override的多型。也就是說,子物件可以繼承父物件的方法或屬性,然後需要時可以override。繼續上節的例子,把子物件的myfoo與mybar改成foo與bar的話,a.foo及a.bar呼叫的就是子物件中定義的方法了。wiki中把這個叫做override的多型:http://en.wikipedia.org/wiki/Polymorphism_in_object-oriented_programming,所以把接下來的分享放到這一節。
Javascript透過prototype chain的方式達成override多型,其實原理很簡單,碰到一個成員敘述時,會先在物件本身找是否有這個identifier的定義,然後再到prototype屬性中找,如果有繼承,那prototype屬性就是父物件,還找不到,就到父物件的prototype屬性裡面找...等等。(剛剛在繼承的部份應該看得很清楚了,每個子物件的prototype屬性就是他的父物件實體,父物件實體也有prototype屬性,就是他的父物件...如此會形成一個「子物件.prototype.prototype.prototype....」的結構,所以叫做prototype chain)。只要在子物件裡面找到identifier的定義,就不會繼續找下去,所以就產生了override的效果。
如果子物件還想執行父物件中被override的方法怎麼辦呢?可以修改一下prototype繼承的做法:
var Parent = function() {
this.foo = function() {
alert('Parent.foo');
};
};
Parent.prototype = Parent;
Parent.bar = function() {
alert('Parent.bar');
};
var Child = function() {
Parent.call(this);
this.foo = function() {
this.superClass.foo.call(this);
alert('Child.foo');
};
};
Child.prototype = new Parent();
Child.prototype.superClass = new Parent();
Child.prototype.bar = function() {
alert('Child.fbar');
};
var a = new Child();
a.foo();
a.bar();
這樣就可以透過superClass屬性存取Parent,而不會被prototype chain規則擋住。
更動態的繼承
Crockford的Javascript:優良部份有介紹一些更動態的繼承方法,簡單地說,就是用for...in來列舉父物件的屬性與方法,然後拷貝到子物件裡。另外,可以用hasOwnProperty來過濾掉從prototype chain來的東西。一般來說,除非需要很嚴謹龐大的繼承體系,我們其實也很少用到Javascript的繼承,在需要的時候,通常只要做像這樣的動態繼承其實就夠了。
Javascript只有prototype繼承的能力,但是很多人還是比較喜歡用class繼承,如果是這樣的話,可以試試Prototype這個框架:http://www.prototypejs.org/,透過他的Class相關API,可以讓你用比較像class繼承的方式來使用Javascript,不過當然底層還是使用prototype繼承的。